录制与回放
基于 rts-server-golang replay/ 模块解析 关键词: 录制, 回放, 确定性验证, 作弊检测, RPLY1格式
概述
录制与回放系统将游戏过程完整记录下来,用于:
- 观战:让其他人观看已结束的对局
- 调试:回放任意历史帧,定位 desync 根因
- 作弊检测:对比录制与实际执行结果,检测篡改
- AI 训练:用真实对局数据训练 AI
帧同步天然适合录制——只需要记录每帧的输入(命令),不需要记录状态。回放时用相同 seed 重跑即可。
文件格式 (RPLY1)
Header (固定长度 + 变长 snapshot):
magic[6] "RPLY1\0" — 格式标识
proto_ver(2) uint16 — wire 协议版本
seed(8) uint64 — 世界 seed
tick_rate(2) uint16 — 每秒帧数
start_ts(8) int64 — 开始时间戳
players(1) uint8 — 玩家数量
mapW(4) int32 — 地图宽(原始值)
mapH(4) int32 — 地图高(原始值)
snap_len(4) uint32 — 初始快照长度
snapshot[] []byte — 初始世界序列化
Tick Stream (每帧重复):
tick(4) uint32 — 帧号
cmd_count(2) uint16 — 命令数量
cmds[] []Cmd — 每条 22 字节
hash(8) uint64 — 该帧执行后 world hash魔数 RPLY1\0
文件头 6 字节唯一标识格式,解析前先校验魔数:
go
if data[0] != 'R' || data[1] != 'P' || data[2] != 'L' ||
data[3] != 'Y' || data[4] != '1' || data[5] != 0 {
return nil, 0, ErrBadMagic
}Cmd 固定 22 字节
player(1) + op(1) + unitid(4) + tx(4) + ty(4) + targetid(4) + tick(4) = 22设计:命令定长,无需长度前缀,方便随机访问和流式处理。
TickRecord
go
type TickRecord struct {
Tick uint32
Cmds []wire.Cmd
Hash uint64
}Hash 字段用于回放时校验——重跑结果与录制 hash 不一致说明有问题。
Writer — 录制
go
type Writer struct {
w io.Writer
written bool // header 只能写一次
}
// 写 Header(游戏开始时调用一次)
func (rw *Writer) WriteHeader(h *Header) error {
data := MarshalHeader(h)
_, err := rw.w.Write(data)
if err == nil {
rw.written = true
}
return err
}
// 写单帧(每帧调用一次)
func (rw *Writer) WriteTick(tick uint32, cmds []wire.Cmd, hash uint64) error {
tr := &TickRecord{Tick: tick, Cmds: cmds, Hash: hash}
data := MarshalTick(tr)
_, err := rw.w.Write(data)
return err
}调用时机在 Room.sealTick() 里:
go
replayWriter.WriteTick(r.tick, cmds, sim.Hash(r.world))Reader — 回放
go
type Reader struct {
data []byte
offset int
Header *Header
}
// 从头到尾逐帧读取
func (r *Reader) NextTick() (*TickRecord, error) {
if r.offset >= len(r.data) {
return nil, nil // EOF
}
tr, n, err := UnmarshalTick(r.data[r.offset:])
r.offset += n
return tr, err
}
// 一次性读完全部帧
func (r *Reader) ReadAll() ([]TickRecord, error)回放流程
go
// 1. 创建 Reader
reader, err := replay.NewReader(data)
// 2. 重建初始世界
world, err := sim.Unmarshal(reader.Header.Snapshot)
// 3. 逐帧重放
for {
tr, err := reader.NextTick()
if tr == nil { break }
sim.Step(world, tr.Cmds)
if sim.Hash(world) != tr.Hash {
return ErrDesync // 校验失败
}
}在 Server 中的集成
go
// cmd/server/main.go
registry := room.NewRegistry(defaults, log)
listener, _ := transport.Listen(transport.ListenerConfig{...})
// Room 创建时附带 replay writer
rm := registry.GetOrCreate(roomID)
rm.SetReplayWriter(replayWriter) // 写入文件录制文件通常写到 /var/replays/room-<id>-<timestamp>.rply。
用途场景
| 场景 | 怎么用 |
|---|---|
| 观战 | 下载 replay 文件,客户端独立回放(不连服务器) |
| Desync 调试 | 找到 hash 不一致的帧,对比两端的 world 差异 |
| 作弊检测 | 服务器录制 + 客户端上报 hash,服务器对比 |
| AI 训练 | 用 replay 数据训练决策模型 |
相关
- 项目源码: rts-server-golang
/internal/replay/ - 上篇: 03_确定性模拟
- 下篇: 05_二进制协议设计